閱讀本篇文章前,仔細想想看
TypeScript 的型別系統與介面之間在語法上的差別與介面或型別的使用有何共通點呢?但意義上又會差在哪?
如果還沒理解完畢的話,可以參考關於 Interface 系列的這幾篇:
- Day 12. 介面宣告 X 使用介面 - TypeScript Interface Intro.
- Day 13. 介面的延伸 X 功能與意義 - Interface Extension & Significance
- Day 14. 函式超載 X 究極融合 - Function Overload & Interface Merging
- Day 15. 功能多樣性 X 多樣性介面 - More on TypeScript Interface
亦或者來看這一篇!
本篇是筆者紀錄自己認為 —— 正確使用 type
與 interface
的方式。
所以這一篇不屬於官方,但也可能也不屬於世界上任何一篇文章。(可能有人會跟我的想法相似?)
另外,本篇可能會跟 Day 13. 內容有相似的地方,但是是綜合所有介面與型別的功能與意義比較。
以下正文開始!
本篇章強烈要求讀者,能夠對 Day 02. 講到關於型別推論與註記 累積到現在的知識,越熟越好!畢竟是 Type 和 Interface 各層面之間的比較 —— 總集篇的概念。
上網時,覺得有些 Medium 文章的作者對 TypeScript 的 type
與 interface
應用層面的敘述,依然感到模糊 —— 並不是語法部分不熟,而是沒有把運用時機講清楚。
來自外國的 TypeScript 開發者,有些文章就直接寫:
“儘管語法宣告不ㄧ樣,應用層面大部分都是一樣,因此你用
type
跟用interface
都沒差”
他們確實有告訴讀者到底 type
跟 interface
的語法規則跟功能差異在哪裡,但問題來了 —— 筆者沒有看到他們回答如何在哪個情境下使用 type
或者是 interface
。
上面那句話確實是對的!type
與 interface
型別註記起來過後的行為都一樣。
定義出一個型別或一個介面,只要它被註記在變數上,該變數存的值若忘記型別註記裡要求的屬性,亦或者是屬性對應的型別錯誤 —— 變數被註記的是 interface
還是 type
,基本上都沒差,TypeScript 照樣會找出潛在問題的發生點,註記(Annotate)過後的行為都差不多。
更不用說:上一篇 筆者介紹的更多介面的功能 —— Indexable Types、readonly
屬性、選用屬性等,也都可以使用在 type
的宣告上。
筆者唯一不苟同的是 —— 文章講完語法上差別之後,就沒有講到意義上的差別。另外,TS 官方在 interface
跟 type
上面的比較,寫得還是讓筆者看不太懂,因此特地再網上翻了一下 StackOverflow,結果找到這篇很完整的分析它們之間的差別;然而,得出的結論是:官方 Doc 是 Outdated 的狀況。
恩...... 哈哈!(這是要叫筆者怎麼推人入坑,筆者想辦法找到當初寫 Doc 的人,先把他們推入火坑再說)
不過這也是學習 TypeScript 遇到的雷,但是越多人知道,相信要更新也不是時間的問題!
當然也有人貼 TypeScript Type 跟 Interface 的差別,但光是看這個表:
連筆者必須誠實的說:“短時間內沒辦法把所有的狀況跟讀者交代清楚啊!”
因此,以下是筆者暴力地把這兩個纏來纏去的東西理出來的結果。今天會把整個系列提及到 interface
與 type
這兩者的相關內容都用比較的方式整理出來。
這應該對讀者來說很簡單。 XD
type T = U;
interface I {
P: U;
...
}
型別化名就是用 type
;介面宣告則是用 interface
。
只有介面 interface
可以被擴展 (使用 extends
):
參照 Day 13. 重點 1.。
只有介面可以被重複宣告,但重複宣告的行為 —— 最後是交集的成果:
參照 Day 14. 重點 2.。
介面的定義格式除了單純物件格式外,也可以單純定義函式型別的介面(亦或者混合型介面);而根據 Indexable Types,介面可以模仿陣列的行為。
但介面無法單純模仿原始型別、列舉與元組,只能用物件格式進行混用。只有型別系統 type
才能單純化名原始型別、列舉與元組。
而 JSON 物件、函式與陣列的共通點是JS 物件的各種表現形式(又被筆者稱作廣義物件),因此才可以被介面描述出來。
不管是宣告型別 T
或介面 I
,基礎的 TypeScript 型別檢測基本上行為都一模ㄧ樣。若某變數 A
被註記型別 T
或被註記介面 I
:
A
存的值少了型別 T
或介面 I
規範的屬性:被 TypeScript 警告A
存的值多出原先沒有在型別 T
或介面 I
規範的屬性:被 TypeScript 警告A
存的值之某屬性在型別 T
或介面 I
裡有規劃,但該屬性存的值之型別跟 T
或 I
規範的不ㄧ樣:被 TypeScript 警告由於介面跟型別都可以表示廣義物件格式(包含普通 JSON 物件、函式、陣列、類別與類別建構的物件),因此可以推論,以下功能兩者都可以使用:
以下也是可以使用的功能,卻沒被筆者提及過:
union
與 intersection
),甚至也可以介面與型別一起複合
這一點在 Day 13. 有討論過,但 Medium 文章上面有這種說法的案例層出不窮,讓筆者感到煎熬。
筆者認為 —— 介面可以藉由 extends
進行擴展;然而,儘管型別可以藉由 intersection
進行複合,但個人認為那不叫做型別的擴展,那應該被稱做定義新的靜態型別。所有提到這個詞 —— Type Extension 根本就是誤導靜態型別的意義。
以下就把筆者剛剛提到的介面與型別混用的狀況討論清楚。
假設已宣告某型別 T
與某介面 I
,並且運用複合型別宣告新型別化名 U
:
我們稱這個作法的意義為:“定義新的型別化名 U
,其型別的靜態格式為型別 T
的內容與介面 I
的屬性與方法”
假設已宣告某型別 T
與某介面 I
,並且宣告新介面 J
,其中型別 T
不為原始型別,則:
我們稱這個作法的意義為:“宣告新的介面 J
,任何變數註記此介面,必須實踐出型別 T
的靜態屬性與介面 I
的所有功能”
不過讀者看到這裡,可能會問:
為何介面與型別混用的狀況是可被接受的?
筆者給出的個人見解是:如果混用狀況不能被接受的話,型別化名的宣告會比較限制些。
比如,我們也可以幫靜態型別定義一些介面 —— 將靜態的屬性格式進行拆解,抽象化的動作。
這樣一來,介面的運用上,除了可以被介面與類別使用外,也可以延伸出靜態資料的格式,也就是型別化名。
這只是筆者想到的其中一個原因,不過個人相信應該還有更深的理由在。當初筆者也是認為介面與型別混用在一起是很怪的行為,可是後來發覺,不混用好像也不對,限制反而又被限制太多。
貼心小提示
介面與類別的使用會在 Day 25. 提到,因為類別可以討論的東西很多;再者,TypeScript 類別比 ES6 Class 又多了更多 Feature 可以使用,因此考慮過後,筆者以沒有使用過 Class 為前提,重新認識 Class 的語法與概念,一步一步地進入主題。
相信有看到 Day 13. 的文章的讀者應該知道 Type 與 Interface 意義差別在哪裡。
介面(Interface)代表更有彈性的型別表現形式,跟規格的概念很像,也具備擴充功能(Interface Extension)與融合功能(Interface Merging)。
型別(Type)代表靜態的型別格式,因此不建議將交集(intersection
)的行為想成擴充型別的行為,而是 —— 宣告新的靜態型別格式,延用交集前的型別靜態格式或介面的規格。
乍看之下感覺好像都可以使用,不過筆者非常強調兩者之間的不同。
本篇唯一重點. Type 與 Interface 使用建議
以下為筆者認定的
type
跟interface
使用的建議:
- 遇到不希望被人擴充、單純想代表一種獨立的資料格式的概念時,使用型別的宣告
type
- 如果單純是原始型別或者是要表示為列舉型別、元組型別,一定只能使用
type
進行宣告- 型別複合(使用
union
或intersection
)的過程通常都是使用type
進行宣告- 遇到功能是可以被重複再利用,該功能可能會被多方程式碼或第三方套件依賴,使用介面的宣告
interface
- 物件格式通常建議用
interface
,使用起來彈性較大- 混合使用型別與介面是可以的,但就是要記得:程式碼到底想要表達的重點是什麼?
- 混用過後不希望再被擴充或代表獨立靜態的型別格式就應該要用型別化名的宣告
type
,藉由union
或intersection
達成- 混用過後的結果是可以被擴充或多方利用,則應該要定義成介面,藉由
extends
去達成
本篇文章雖然字數算是系列中偏少的,但筆者認為 —— 塞再多也沒用,因為都是繞在 Type 跟 Interface 的比較。重點在能不能分辨什麼時機運用 Type 或者是 Interface 是本篇主要想傳達的重點。
下一篇筆者要補足複合型別(union
與 intersection
)的註記與使用意義~ 敬請期待吧~